import logging
from time import sleep           # only used by TextView.highlight
import re                        # see View.parse_dimension
import color     # definitions of colors in the required xml format

# --- module logger
logger = logging.getLogger(__name__)

# --- handy abbreviations for views/layouts ---
Horiz = "horizontal"
Vert = "vertical"
WrapC = "wrap_content"
FillP = "fill_parent"      # deprecated: use MatchP
MatchP = "match_parent"
ZeroDP = "0dp"

# --- keycodes defined as string as returned by the event handler --- 
KEYCODE_BACK="4"
KEYCODE_MENU="82"
KEYCODE_SEARCH="84"
KEYCODE_VOLUME_UP="24"
KEYCODE_VOLUME_DOWN="25"



class ViewError(Exception):
  """Generic Exception raised by the library"""
  pass


class View:
  """View class: top of the hierarchy.
  Correspond to the android.view.View class in Java
  Represent a generic View element
  
  Note on id's: each View has an id which is used to identify it for example
  in the setProperty method. Views (more procisely ViewGroups) can contain
  other views so we can have a hierarchy of views. 
  When we invoke the show method of a view element v, the UI defined by the 
  subtree rooted at v is displayed and v becomes the current root element.
  
  The root element must be retained since it is the only one that can be 
  used to change the properties of any displayed view, or to dismiss
  the UI (via the dismiss method). The root element is the only one containing
  in the variable droid the current Android instance (all other elements 
  have the droid variable set to None).
  
  When the show method is invoked it stores in root.displayed_ids
  a dict that associates to each view id the View instance with that id 
  (an error occurs if two views have the same id, even if android allows it).
  When the (say) root.setProperty(viewid,attr,value) method is called  
  the View instance with id viewid is retrieved via the dict displayed_ids 
  and its _set_attr method is called. 
  """
  # class constants to produce the root xml 
  xmlns = """xmlns:android="http://schemas.android.com/apk/res/android" """
  xml_header = """<?xml version="1.0" encoding="utf-8"?>\n"""
  # template for xml
  template = """<%(name)s %(xmlns)s
    android:id="@+id/%(id)s"
    android:layout_width="%(width)s"
    android:layout_height="%(height)s"\n%(xml_additional_attrs)s/>"""
  
  def __init__(self,sid,height,width,xml_attrs):
    self._id = sid               # id of this element: private!
    self.height = height
    self.width = width 
    self.xml_attrs = xml_attrs   # additional xml attributes         
    self._changed_attrs = {}     # attrs changed by _set_attr after view is displayed
    self.droid = None            # Android instance currently showing the view 
    self.name = "View"           # name used to create the xml representation
    self.listeners = {}

  # methods dealing with id's
  def contains(self,some_id):
    """True if self contains a view with id some_id"""
    return self._id == some_id    # View with no children... 

  def contained_ids(self):
    """Return the dict id:View of all contained View elements"""
    return {self._id:self}
         
  # --- methods show/dismiss       
  def show(self,droid):
    """Show self on droid""" 
    self.droid = droid        # save for future modifications and dimissing 
    s = self.xml(True)        # get xml root representation
    s = View.xml_header + s
    logger.debug("[fullShowXML] " + s)
    out = droid.fullShow(s)
    logger.info("[fullShow] " + str(out))
    self.displayed_ids = self.contained_ids() # record contained id's at the time of showing
    self._clear_changed_attrs()    # clear _changed_attrs dict for all contained view   
    
  def dismiss(self,full=False):
    """Dismiss self (must be root of View tree), if full=True call fullDismiss
    
    Unless full=True the dismission is only logical (no operations are 
    allowed): the view remains on the screen. A logical dismiss is used when
    the app must display something different: with a full dismiss the user
    would see for a short time the display of another app
    """
    if self.droid is not None:
      if full:
        out = self.droid.fullDismiss()
        logger.info("[fullDismiss] " + str(out)) 
      else:
        logger.info("[dismiss]")  
      self.droid = None        # View is no longer used
    else:
      raise ViewError("[dismiss] View not currently displayed")

  # --- methods dealing with attributes 
  def setXmlAttribute(self,attr,value):
    """Set an attribute in the xml representation
    
    Note that this method is conceived to be invoked before a View is displayed 
    since a change of and attribute here has no effect on an already displayed view
    (the effect will be in the next show() invocation). To change an attribute 
    of a displayed View with immediate effect use method setProperty 
    """
    self.xml_attrs[attr]=value  

  def multiSetXmlAttribute(self,attr,id_val_dict):
    """TOBE TESTED Change xml attribute attr for descendant Views in id_val_dict
    
    The method as no innediate effect on displayed Views. Use multiSetProperty
    for that. See note for the setXmlAttribute method
    """
    # Generic View has no children so only self._id has to be checked
    if self._id in id_val_dict.keys():
      self.setXmlAttribute(attr,id_val_keys[_id])

  def setMargins(self,l,r,t,b,units="dp"):
    """Set left/right/top/bottom margins on the xml representation"""
    self.xml_attrs["layout_marginLeft"]=str(l)+units
    self.xml_attrs["layout_marginRight"]=str(r)+units
    self.xml_attrs["layout_marginTop"]=str(t)+units
    self.xml_attrs["layout_marginBottom"]=str(b)+units

  # ---- methods for displayed items
  def setProperty(self,viewid,attr,value):
    """Change attribute of a displayed View descendant of self""" 
    if self.droid is None: 
      raise ViewError("[setProperty] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      self.displayed_ids[viewid]._set_attr(self.droid,attr,value)
    else:
      raise ViewError("[setProperty] Unknown id %s in View %s" % (viewid,self._id))

  def multiSetProperty(self,attr,id_val_dict):
    """Change property attr for all displayed Views in id_val_dict"""
    if self.droid is None: 
      raise ViewError("[multiSetProperty] View %s not currently displayed" % self._id)
    for viewid in id_val_dict:  
      if viewid in self.displayed_ids.keys():
        self.displayed_ids[viewid]._set_attr(self.droid,attr,id_val_dict[viewid])
      else:
        raise ViewError("[multiSetProperty] Unknown id %s in View %s" % (viewid,self._id))
    
  def _set_attr(self,some_droid, attr,value):
    """Change an attribute of the current view. Record the new value in _changed_attrs"""
    out = some_droid.fullSetProperty(self._id,attr,str(value))
    logger.info("[_set_attr] " + str(out))
    if out.error is not None:
      raise ViewError("[_set_attr] "+str(out)) 
    self._changed_attrs[attr] = str(value)  

  def _clear_changed_attrs(self):
    """Clear the _changed_attrs dict, invoked by the show method"""
    self._changed_attrs.clear()

  def _get_current_attr(self,attr):
    """Get the current value of attribute attr"""
    if attr in self._changed_attrs.keys():
      return self._changed_attrs[attr]
    if attr in self.xml_attrs:
      return self.xml_attrs[attr]    
    else:
      return None

  def scaleText(self,ratio):
    """if self is the root of a displayed View multiply by ratio the textSize
    of any contained View"""
    if self.droid is None: 
      raise ViewError("[scaleText] View %s not currently displayed" % self._id)
    for viewid in self.displayed_ids.keys():
      size = self.displayed_ids[viewid]._get_current_attr("textSize")
      if size: 
        # parse value and units inside size.  
        # the parsing consists in 0+ whitespaces, a float(->val) and everything else (->units) 
        m = re.match("\s*(?P<val>[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)(?P<units>.*)",size,re.DOTALL)
        try:
          val = float(m.group("val"))
          units = m.group("units")
          newval = val*ratio  
          self.displayed_ids[viewid]._set_attr(self.droid,"textSize",str(newval)+units)
        except:
          # if something wrong don't change size and log an exception
          logger.exception("[scaleText] Unable to parse size %s in View %s" % (size,self._id))

  # --- hide/unhide/highlight
  def hide(self,viewid):
    """Hide the TextView with id viewid: only for displayed items""" 
    if self.droid is None: 
      raise ViewError("[hide] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      self.displayed_ids[viewid]._hide(self.droid)
    else:
      raise ViewError("[hide] Unknown id %s in View %s" % (viewid,self._id))

  def unhide(self,viewid):
    """Unhide the TextView viewid restoring the last known background and text""" 
    if self.droid is None: 
      raise ViewError("[hunide] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      self.displayed_ids[viewid]._unhide(self.droid)
    else:
      raise ViewError("[unhide] Unknown id %s in View %s" % (viewid,self._id))

  def highlight(self,viewid,color=color.name("AliceBlue"),delay=0):
    """Highlight TextView viewid: only for displayed items""" 
    if self.droid is None: 
      raise ViewError("[highlight] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      self.displayed_ids[viewid]._highlight(self.droid,color,delay)
    else:
      raise ViewError("[highlight] Unknown id %s in View %s" % (viewid,self._id))

  def queryDetail(self,viewid):
    """get detailed information for a displayed widget""" 
    if self.droid is None: 
      raise ViewError("[queryDetail] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      self.displayed_ids[viewid]._queryDetail(self.droid)
    else:
      raise ViewError("[queryDetail] Unknown id %s in View %s" % (viewid,self._id))

  def _queryDetail(self,some_droid):
    """get detailed information for current widget""" 
    r = some_droid.fullQueryDetail(self._id)
    logger.info("[_queryDetail] " + str(r))
    if r.error is not None:
      raise ViewError("[_queryDetail] "+str(r))
    return r.result

  def setList(self,viewid,itemlist):
    """Set the list associated to an adapter descendant of self""" 
    if self.droid is None: 
      raise ViewError("[setList] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      self.displayed_ids[viewid]._setList(self.droid,itemlist)
    else:
      raise ViewError("[setList] Unknown id %s in View %s" % (viewid,self._id))

  # methods dealing with events          
  def setListener(self,event,action):
    """TODO Associate a listener to a specific event"""
    self.listeners[event] = action

  def getItemPosition(self,viewid):
    """Get the currently selected Spinner item"""
    if self.droid is None: 
      raise ViewError("[getItemPosition] View %s not currently displayed" % self._id)
    if viewid in self.displayed_ids.keys():
      return self.displayed_ids[viewid]._getItemPosition(self.droid)
    else:
      raise ViewError("[getItemPosition] Unknown id %s in View %s" % (viewid,self._id))

  # methods dealing with xml
  def xml(self,root=False):
    """xml representation of the View"""
    xmlns = View.xmlns if root else ""
    s = View.template % {
         "name":self.name,
         "xmlns":xmlns,
         "id":self._id,
         "height": self.height,
         "width":self.width,
         "xml_additional_attrs":self._translate_attrs()
        }
    return s

  def _translate_attrs(self):
    """build an "android:attr=value" line for each item in xml_attr"""
    attrs = ""
    for (a,v) in self.xml_attrs.items():
      attrs += """    android:%s="%s"\n""" % (a,v)
    return attrs

  # other methods
  def __str__(self):
    """Return a simple (non-recursive) xml representation"""
    return View.xml(self,False)
          

# -----TextView is the main single item View  
class TextView(View):
                  
  def __init__(self, sid, height, width, xml_attrs):
     View.__init__(self,sid, height, width, xml_attrs)
     self.name="TextView"

  def xml(self,root=False):
    if "text" not in self.xml_attrs.keys():
      raise ViewError('[xml] Missing "text" attribute in TextView')
    return View.xml(self,root)

  def _hide(self,some_droid):
    """Hide the TextView"""
    self._hide_saved_text = self._get_current_attr("text")
    self._hide_saved_backgr = self._get_current_attr("background")
    self._set_attr(some_droid,"text","") 
    self._set_attr(some_droid,"background","#00000000") #transparent black
    
  def _unhide(self,some_droid):
    """Restore the TextView to the last known status"""
    self._set_attr(some_droid,"text",self._hide_saved_text)
    self._set_attr(some_droid,"background",self._hide_saved_backgr)

  def _highlight(self,some_droid,color,delay):
    """Highlight the TextView by setting background to color"""
    saved_backgr = self._get_current_attr("background")
    self._set_attr(some_droid,"background",color)
    if delay>0:
      sleep(delay)
    self._set_attr(some_droid,"background",saved_backgr)   


class Button(TextView):
                  
  def __init__(self, sid, height, width, xml_attrs):
     View.__init__(self,sid, height, width, xml_attrs)
     self.name="Button"


class EditText(TextView):
                  
  def __init__(self, sid, height, width, xml_attrs):
     View.__init__(self,sid, height, width, xml_attrs)
     self.name="EditText"



# --- ViewGroup and subclasses can have children View

class ViewGroup(View):
  # template for the non-root xml
  template = """<%(name)s %(xmlns)s
    android:id="@+id/%(id)s"
    android:layout_width="%(width)s"
    android:layout_height="%(height)s"\n%(xml_additional_attrs)s>
%(items)s</%(name)s>"""

  def __init__(self, sid, height, width, xml_attrs):            
    View.__init__(self,sid, height, width, xml_attrs)
    self.children = []
    self.name = "ViewGroup"

  def contains(self,some_id):
    """True if self contains a view with id some_id"""
    if self._id == some_id: return True 
    for v in self.children:
      if v.contains(some_id): return True
    return False  

  def contained_ids(self):
    """Return the dict id->View of all contained View elements"""
    tmp = {self._id:self}
    for v in self.children:
      t = v.contained_ids() # dict for subview v
      for (i,w) in t.items():
        if i in tmp.keys():
          raise ViewError('Duplicate id "%s" in View "%s"' % (i,self._id))
        tmp[i] = w
    return tmp

  def _clear_changed_attrs(self):
    """Clear the _changed_attrs dict, invoked by the show method"""
    self._changed_attrs.clear()
    for v in self.children:
      v._clear_changed_attrs()     

  def addView(self,v):
    """Add a View at the end of the children list"""
    self.children.append(v)

  def insertView(self,i,v):
    """Add a View in position i of the children list""" 
    self.children.insert(i,v)

  def multiSetXmlAttribute(self,attr,id_val_dict):
    """TOBE TESTED Change xml attribute attr for descendant Views in id_val_dict
    
    The method as no innediate effect on displayed Views. Use multiSetProperty
    for that. See note for the setXmlAttribute method
    """
    # check self._id
    if self._id in id_val_dict.keys():
      self.setXmlAttribute(attr,id_val_keys[_id])
    # change attr in all descendants   
    for v in self.children:
      v.multiSetXmlAttribute(attr,id_val_dict)


  def xml(self,root=False):
    """xml representation of the ViewGroup"""
    xmlns = View.xmlns if root else ""
    xml_items = ""
    for i in self.children:
      xml_items += i.xml() + "\n"    
    s = ViewGroup.template % {
         "name":self.name,
         "xmlns":xmlns,
         "id":self._id,
         "height": self.height,
         "width":self.width,
         "xml_additional_attrs":self._translate_attrs(),
         "items":xml_items
        }
    return s

  
class LinearLayout(ViewGroup):
 
  def __init__(self, sid, height, width, xml_attrs):            
    ViewGroup.__init__(self,sid, height, width, xml_attrs)
    self.name = "LinearLayout"
      
  def xml(self,root=False):
    if "orientation" not in self.xml_attrs.keys():
      raise ViewError('[xml] Missing "orientation" attribute in LinearLayout')
    return ViewGroup.xml(self,root)


class RelativeLayout(ViewGroup):
 
  def __init__(self, sid, height, width, xml_attrs):            
    ViewGroup.__init__(self,sid, height, width, xml_attrs)
    self.name = "RelativeLayout"


class FrameLayout(ViewGroup):
 
  def __init__(self, sid, height, width, xml_attrs):            
    ViewGroup.__init__(self,sid, height, width, xml_attrs)
    self.name = "FrameLayout"


class ScrollView(FrameLayout):
 
  def __init__(self, sid, height, width, xml_attrs):            
    ViewGroup.__init__(self,sid, height, width, xml_attrs)
    self.name = "ScrollView"


  
    
class AdapterView(ViewGroup):

  def __init__(self, sid, height, width, xml_attrs):            
    ViewGroup.__init__(self,sid, height, width, xml_attrs)
    self.name = "AdapterView"

  def _setList(self,some_droid, some_list):
    """Set the list of the current adapter"""
    out = some_droid.fullSetList(self._id,some_list)
    logger.info("[_setList] " + str(out))
    if out.error is not None:
      raise ViewError("[_setList] "+str(out))

  
class ListView(AdapterView):
  def __init__(self, sid, height, width, xml_attrs):            
    AdapterView.__init__(self,sid, height, width, xml_attrs)
    self.name = "ListView"
  

class Spinner(AdapterView):
  
  def __init__(self, sid, height, width, xml_attrs):            
    AdapterView.__init__(self,sid, height, width, xml_attrs)
    self.name = "Spinner"
  
  def _getItemPosition(self,some_droid):
    r = some_droid.fullQueryDetail(self._id)
    logger.info("[_getItemPosition] " + str(r))
    if r.error is not None:
      raise ViewError("[_getItemPosition] "+str(r))
    return int(r.result["selectedItemPosition"])
  
  

# i colori html hanno alpha=0 quindi sono in pratica inutilizzabili 


# gli spinner non generano eventi, ma il settaggio corrente si puo' leggere 
# in selectedItemPosition attraverso un 
# x = droid.fullQueryDetail("spin")
# x.result['selectedItemPosition']

# sfortunatamente, 'selectedItemPosition' non lo si puo' settare ne
# mediante l'XML iniziale ne con fullSetProperty

# quindi gli unici eventi da gestire mi sembrano click/itemclick/key

"""
Results of tests on attribute setting:

attribute            XML    fullSetProperty    fullQueryDetail/event
alpha                 N           N                  N
background            Y           Y                   
text                  Y           Y
layout_weight         Y           Y(non subito)      N
layout_margin         N           N                  N
layout_marginLRTB     Y           N                  N
layout_marginES       ?           ?                  ?  (end/start) 
padding               Y           Y                  ?
paddingLRTB           N           N
selectedItemPosition                                 Y (for spinner)
visibility            N           N                  Y (always 0)
textStyle             Y           Y

Case counts in the above attributes!!!!
"""


"""
Eventi:

{u'data': {u'text': u'Bottone 1', u'type': u'Button', u'id': u'b1', u'visibility': u'0'}, u'name': u'click', u'time': 1366115595582000}
{u'data': {u'position': u'0', u'selectedItemPosition': u'-1', u'type': u'ListView', u'id': u'lista', u'visibility': u'0'}, u'name': u'itemclick', u'time': 1366115611381000}
{u'data': {u'position': u'1', u'selectedItemPosition': u'-1', u'type': u'ListView', u'id': u'lista', u'visibility': u'0'}, u'name': u'itemclick', u'time': 1366115613500000}
{u'data': {u'position': u'2', u'selectedItemPosition': u'-1', u'type': u'ListView', u'id': u'lista', u'visibility': u'0'}, u'name': u'itemclick', u'time': 1366115615530000}
"""




